Repository navigation
feat(github): protect vaulted tags against deletion and force-push - #7
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 12 12
Lines 1074 1105 +31
=========================================
+ Hits 1074 1105 +31 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
b4eaac1 to
b9f1127
Compare
The README's first promise is that uvault "pushes them as immutable tags", but nothing made them immutable: a tag can be deleted or force-moved by anyone with push access to the vault, which is the same failure mode vaulting exists to prevent, just relocated from upstream to the vault. Adds `GitHubForge.ensure_tag_ruleset(repo_path)`, which applies a tag ruleset blocking `deletion` and `non_fast_forward`, and calls it after a successful fork. Deliberately takes a repository path rather than deriving one from the source, so the same primitive serves a per-upstream fork and a single shared vault repository. That keeps it useful whichever topology the discussion in petrus-v#6 settles on, and it is the piece a `protect-tags` subcommand would reuse unchanged. Three properties that matter, each with a test: - **`creation` is never blocked.** It is the obvious third rule and it would break every later sync, because the vault has to keep accepting new tags. Only `deletion` and `non_fast_forward`. - **It never raises.** Creating a ruleset needs `Administration: write`, strictly more than forking and pushing, so a token that cannot do it must still be able to vault. 403 and 404 get specific messages -- 404 being what a plan without rulesets looks like -- and anything else is warned about and swallowed. `fork()` therefore still succeeds when the tags cannot be protected. - **It is idempotent.** An existing ruleset with the same name and target is left alone, so re-running costs one GET. Coverage stays at 100% in the gating `--extra github` run.
b9f1127 to
587512a
Compare
petrus-v
left a comment
There was a problem hiding this comment.
could you consider adding some reference documentation in uvault sync to explain that a uvault-tags-immutable ruleset will be automatically setup on the vault repository ?
Review found that `non_fast_forward` alone does not make a tag immutable: force-pushing over an existing tag still succeeded. Tested on a real repository, the `update` restriction is what prevents it, so the ruleset now carries `deletion`, `non_fast_forward` and `update`. Blocking updates costs nothing here. uvault pushes `<sha>:refs/tags/<tag>` with no `+` refspec and no `--force` (vcs.py `vault_tag` / `vault_release_tag`), so it never moves a tag that already exists. `creation` remains deliberately unrestricted -- that one really would break every later sync. Also adds the requested reference documentation under `uvault sync`, next to the existing automatic-forking section: what the ruleset is called, which three rules it applies and why, that creation is left alone, and that the whole thing is advisory -- a token without `Administration: write` still vaults, with a warning. Notes the two cases that surprise people: rulesets need a public repository on GitHub Free, and repositories forked before this existed are not retrofitted.
|
Thanks for actually testing it, that caught a real hole. I had checked that the Added Restricting updates sounds like it might break re-syncing, so worth saying why it One caveat on my side. The tests check the payload shape against a mocked API, Reference docs added under
That last one is the real gap in this PR. Protection happens at fork time, so
|
Closes part of #6 — the "start simple with support at fork time" piece you
suggested.
Why
The README's first promise is that uvault "pushes them as immutable tags".
Nothing made them immutable: a tag can be deleted or force-moved by anyone with
push access to the vault.
That is the same failure mode vaulting exists to prevent, relocated from upstream to the vault.
What
GitHubForge.ensure_tag_ruleset(repo_path)applies a tag ruleset blockingdeletionandnon_fast_forward, andfork()calls it after a successful fork.It takes a repository path rather than deriving one from the source, so the
same primitive serves a per-upstream fork and a single shared vault repo.
That was deliberate given the direction you raised in #6 — nothing here has to be rewritten if the single-repo layout wins, and a
protect-tagssubcommand would reuse this unchanged across whatever repositories the config names.Three properties, each with a test
creationis never blocked. It is the obvious third rule to add and itbreaks every later sync, because the vault has to keep accepting new tags. Only
deletionandnon_fast_forward.It never raises. Creating a ruleset needs
Administration: write, strictlymore than forking and pushing, so a token that cannot do it must still be able to
vault. 403 and 404 get specific messages — 404 being what a plan without rulesets
looks like — and anything else is warned about and swallowed.
fork()thereforestill returns True when the tags could not be protected, which also covers the
opt-out case you mentioned.
That one is a real guard rather than a theoretical one: an earlier draft raised
ValueErroron an unexpected response shape and propagated it out offork(), failing the fork itself. There is a regression test for it.It is idempotent. An existing ruleset with the same name and target is left alone, so re-running costs one GET.
Notes
Github.requester.requestJsonAndCheck. No private attributes.uvault-tags-immutable) so theidempotency check and any future
protect-tagsagree on it.--extra githubrun (142 passed).Not included, as discussed:
protect-tagsitself, and anything presuming atopology. Happy to follow up with the subcommand once #6 settles, or sooner if
you would rather have it now.